package com.jingdianjichi.subject.infra.config;

import com.jingdianjichi.subject.common.utils.LoginUtil;
import com.jingdianjichi.subject.infra.basic.entity.SubjectMapping;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.stereotype.Component;

import java.lang.reflect.Field;
import java.sql.Statement;
import java.util.*;

/**
 * 填充创建用户,创建时间,等公共字段的拦截器亲,其实就是兰基恩SQL
 */
@Component
@Slf4j                  //执行mysql的时候这里就是执行器来的  //我们的整体处理使用update的方式,其实都是固定的写法亲,就是我们拦截所有的update语句做自定义操作来的
@Intercepts({
        @Signature(method = "update", type = Executor.class, args = {MappedStatement.class, Object.class}),
})
//该注解标记了一个拦截器，用于拦截 MyBatis 执行器（Executor）的 update 方法，允许在 SQL 更新操作（如 INSERT、UPDATE、DELETE）执行前后插入自定义逻辑（如日志记录、数据校验、SQL 改写等）。
/*
    批量插入的底层执行路径不同
    MyBatis 原生批量插入（使用 <foreach> 拼接 SQL）时，底层通过 StatementHandler 直接执行 SQL，而非通过 Executor 的 update 方法。而你的拦截器仅配置了拦截 Executor 的 update 方法，因此无法捕获批量插入操作。
 */
public class MyBatisInterceptor implements Interceptor {

    //这个是最重要的亲,这里就是帮助我们拦截Sql,处理逻辑就在这里亲
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];

        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();    //看看拦截的SQL是啥类型

        Object parameter = invocation.getArgs()[1]; //就是mapper方法中的参数亲,因为只有一个参数我们的mapper

        //没有参数直接放行
        if (parameter ==null){
            return invocation.proceed();
        }

        //用户id
        String loginId = LoginUtil.getLoginId();

        if (StringUtils.isBlank(loginId)){
            //id是空直接放行
            return invocation.proceed();
        }

        if (SqlCommandType.INSERT == sqlCommandType || SqlCommandType.UPDATE == sqlCommandType){
            replaceEntityProperty(parameter,loginId,sqlCommandType);
        }

        //不是这两类的Sql我们放行
        return invocation.proceed();
    }


    //自定义拦截逻辑亲
    private void replaceEntityProperty(Object parameter, String loginId, SqlCommandType sqlCommandType) {
        if (parameter instanceof Map){
            //这里就是map逻辑了
            replaceMap((Map) parameter,loginId,sqlCommandType);         //这里做强转是可以的,因为我们前面已经做逻辑校验了
        }else {
            //我们的拦截有参数是实体的,有参数是map的,这个就是参数是实体的
            replace(parameter,loginId,sqlCommandType);
        }
    }

    //处理List参数的Sql拦截
    private void replaceList(List parameter,String loginId, SqlCommandType sqlCommandType){
        for (Object entity : (Collection) parameter) {
            replace(entity, loginId, sqlCommandType);
        }
    }

    //处理Map参数的sql拦截
    private void replaceMap(Map parameter, String loginId, SqlCommandType sqlCommandType){
        Collection values = parameter.values();

        for (Object obj : values) {
            replace(obj,loginId,sqlCommandType);
        }
    }


    //处理实体参数的sql拦截
    private void replace(Object parameter, String loginId, SqlCommandType sqlCommandType){
        if (SqlCommandType.INSERT == sqlCommandType){
            //如果是插入操作我们就去
            dealInsert(parameter,loginId);
        }else {
            //这里我们就去走更新的逻辑
            dealUpdate(parameter,loginId);
        }
    }


    //自定义拦截sql的逻辑,处理insert逻辑,处理实体参数
    private void dealInsert(Object parameter, String loginId) {
        //利用反射了亲
        Field[] fields = getAllFields(parameter);

        //目的就是如果有我们的目标字段就去做替换,如果出现报错不嫩影响我们的业务逻辑的亲
        for (Field field : fields) {
            try {
                field.setAccessible(true);              //因为我们的字段是private的这样先设置一下才能获得private修饰的属性的值的
                Object o = field.get(parameter);        //我们是填充空字段的,所以如果不是非空的话,我们就直接放行

                if (Objects.nonNull(o)){
                    field.setAccessible(false);        //设置回去,直接放行
                    continue;
                }

                //填充字段
                if ("isDeleted".equals(field.getName()))
                {
                    field.set(parameter,0);
                    field.setAccessible(false);
                }
                else if ("createdBy".equals(field.getName()))
                {
                    field.set(parameter,loginId);
                }
                else if ("createdTime".equals(field.getName()))
                {
                    field.set(parameter,new Date());
                    field.setAccessible(false);
                }
                else
                {
                    field.setAccessible(false);
                }


            }catch (Exception e){
                log.error("dealInsert.error:{}",e.getMessage(),e);  //这里如果出现错误我们就认了
            }
        }
    }


    //自定义拦截sql的逻辑,处理update逻辑,处理实体参数
    private void dealUpdate(Object parameter, String loginId) {
        //利用反射了亲
        Field[] fields = getAllFields(parameter);

        //目的就是如果有我们的目标字段就去做替换,如果出现报错不嫩影响我们的业务逻辑的亲
        for (Field field : fields) {
            try {
                field.setAccessible(true);              //因为我们的字段是private的这样先设置一下才能获得private修饰的属性的值的
                Object o = field.get(parameter);        //我们是填充空字段的,所以如果不是非空的话,我们就直接放行

                if (Objects.nonNull(o)){
                    field.setAccessible(false);        //设置回去,直接放行
                    continue;
                }

                //填充字段
                if ("updateBy".equals(field.getName()))
                {
                    field.set(parameter,loginId);
                    field.setAccessible(false);
                }
                else if ("updateTime".equals(field.getName()))
                {
                    field.set(parameter,new Date());
                    field.setAccessible(false);
                } else
                {
                    field.setAccessible(false);
                }


            }catch (Exception e){
                log.error("dealUpdate.error:{}",e.getMessage(),e);  //这里如果出现错误我们就认了
            }
        }
    }


    //获得这个类的所有字段包括父类的
    private Field[] getAllFields(Object object){
        Class<?> clazz = object.getClass();

        List<Field> fieldList=new ArrayList<>();

        while (clazz != null){
            fieldList.addAll(new ArrayList<>(Arrays.asList(clazz.getDeclaredFields())));  //这里就是获得类的所有字段了

            clazz=clazz.getSuperclass();        //再把父亲的字段放进来,所以这里我们使用的是循环的逻辑
        }

        Field[] fields=new Field[fieldList.size()];
        fieldList.toArray(fields);

        //返回所有的字段
        return fields;
    }


    //这个属于就是把这个拦截器注册进去了
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target,this);
    }

    //这个没用我们不用重写的亲
    @Override
    public void setProperties(Properties properties) {
        Interceptor.super.setProperties(properties);
    }
}
/*
第一个 @Signature：拦截 Executor 的 update 方法
@Signature(method = "update", type = Executor.class, args = {MappedStatement.class, Object.class})
    作用对象：
        /Executor 是 MyBatis 的核心执行器，负责调度 SQL 执行的全流程（如 update、query、commit 等）。
    拦截方法：
        /update 方法对应所有写操作（INSERT/UPDATE/DELETE）的 SQL 执行入口。
    参数说明：
        /args 指定了 update 方法的参数类型，即 MappedStatement（映射语句元数据）和 Object（实际参数对象）。
    典型场景：
        /自动填充公共字段（如 createdTime、updateBy）、审计日志记录、事务管理等。

2.  第二个 @Signature：拦截 StatementHandler 的 update 方法
@Signature(type = StatementHandler.class, method = "update", args = {Statement.class})
    作用对象：
        /StatementHandler 负责 SQL 语句的构建和执行（如 PreparedStatement 的生成）。
    拦截方法：
        /update 方法对应 JDBC 层 Statement 的实际执行动作。
    参数说明：
        /args 指定了 Statement 类型参数，表示直接拦截 JDBC 原生语句的执行。
    典型场景：
        /批量插入（如 <foreach> 拼接的 SQL）、SQL 改写（如分页语句拼接）、性能监控（记录 SQL 执行时间）。

为何需要同时拦截 Executor 和 StatementHandler？
    覆盖不同执行路径：
        1.Executor 的 update 是 MyBatis 高层抽象入口，但某些场景（如原生批量插入）会直接通过 StatementHandler 执行 SQL。
        2.双重拦截确保无论 SQL 通过哪种路径执行，都能触发拦截逻辑。

    参数处理差异：
        1.Executor 拦截的参数是业务层对象（如实体类或 Map），适合处理业务逻辑（如自动填充字段）。
        2.StatementHandler 拦截的参数是 JDBC 原生对象（如 Statement），适合直接操作 SQL 字符串或底层执行细节。
 */
